package org.tulkas.robot;

import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.util.EntityUtils;
import org.tulkas.robot.base.GlobalConfig;
import org.tulkas.robot.exception.SystemException;
import org.tulkas.robot.http.BaseClient;
import org.tulkas.robot.http.BaseConn;
import org.tulkas.robot.http.IConnCallback;
import org.tulkas.robot.http.headers.ResponseHeaders;
import org.tulkas.util.EncodeUtil;
import org.tulkas.util.HttpUtil;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class Browser extends BaseClient {
	public static final String DEFAULT_CLIENT_NAME = "browser-default";

	private volatile ExecutorService asyncExecutor;
	private boolean executorCreated = false;

	public static final String URL_SCHEMA_HTTP = "http";
	private static final Map<String, String> WELL_KNOWN_TYPE = new HashMap<String, String>();

	static {
		WELL_KNOWN_TYPE.put("text/html", "html");
		WELL_KNOWN_TYPE.put("text/xml", "xml");
		WELL_KNOWN_TYPE.put("application/json", "json");
		WELL_KNOWN_TYPE.put("image/jpeg", "jpeg");
		WELL_KNOWN_TYPE.put("image/gif", "gif");
		WELL_KNOWN_TYPE.put("image/png", "png");
		WELL_KNOWN_TYPE.put("application/x-shockwave-flash", "swf");
		WELL_KNOWN_TYPE.put("application/pdf", "pdf");
		WELL_KNOWN_TYPE.put("video/x-flv", "flv");
	}

	private final Set<DownloadItem> downloadItems = new LinkedHashSet<DownloadItem>();

	public Browser() {
		super(DEFAULT_CLIENT_NAME);
	}

	public Browser(String clientName) {
		super(clientName);
	}

	public synchronized void enableAsync(int maxParallel) {
		destoryExecutor();
		asyncExecutor = Executors.newFixedThreadPool(maxParallel);
		executorCreated = true;
	}

	public synchronized void enableAsync(ExecutorService service) {
		destoryExecutor();
		asyncExecutor = service;
		executorCreated = false;
	}

	public void destroy() {
		destoryExecutor();
	}

	protected synchronized void destoryExecutor() {
		if (executorCreated && asyncExecutor != null
				&& !asyncExecutor.isShutdown())
			asyncExecutor.shutdownNow();
	}

	public ExecutorService getAsyncExecutor() {
		if (asyncExecutor == null) {
			synchronized (this) {
				if (asyncExecutor == null) {
					enableAsync(GlobalConfig.getInstance().getInt(
							"browser.default.max.parallel.tasks", 5));
				}
			}
		}
		return asyncExecutor;
	}

	protected void processBaseConn(BaseConn conn, String fromUrl,
			HttpRequestBase request, String logMsg) {
		boolean repeat = false;
		int times = 1;
		int maxTimes = getClientOptions().getFailRepeatTimes();
		do {
			try {
				prepareBasicRequestHeaders(conn, fromUrl);
				conn.resetResponse();
				HttpResponse response = httpExecute(request, conn);
				handleResponseStatus(response, conn);
				OutputStream os = conn.createResponseStream();
				handleResponseData(response, conn, os);
				conn.handleResponseContent(os, getClientOptions());
				repeat = false;
			} catch (IOException e) {
				conn.setException(e);
			}

			logAfterResponse(conn, (times == 1) ? logMsg : "Repeating "
					+ logMsg);

			if (conn.isError()) {
				if (times < maxTimes) {
					times++;
					repeat = true;
				}
			}
		} while (repeat);

	}

	public Page openPage(String url) throws IOException {
		return openPage(url, null);
	}

	public Page openPage(String url, Page from) {
		return openPageInternal(url, from, null, false);
	}

	public void openPageAsync(String url, IPageCallback callback) {
		openPageAsync(url, null, callback);
	}

	public void openPageAsync(String url, Page from, IPageCallback callback) {
		openPageInternal(url, from, callback, true);
	}

	protected Page openPageInternal(String url, Page from,
			IPageCallback callback, boolean async) {
		Page page = new Page();
		page.setUrl(url);

		String fromUrl = null;
		if (from != null)
			fromUrl = from.getUrl();

		try {
			page.setUrl(HttpUtil.checkAndFillHttpUrl(page.getUrl(), fromUrl));
		} catch (MalformedURLException e) {
			throw new SystemException("Bad url in the from page", e);
		}

		HttpRequestBase request = page.createHttpRequest(getClientOptions());
		String logMsg = "Opening page " + page.getUrl();
		if (async) {
			AsyncConnProcess<Page> process = new AsyncConnProcess<Page>(page,
					fromUrl, request, callback, logMsg);
			getAsyncExecutor().execute(process);
			return null;
		} else {
			processBaseConn(page, fromUrl, request, logMsg);
			return page;
		}
	}

	public Page submitForm(Form form) {
		return submitForm(form, null);
	}

	public Page submitForm(Form form, Page from) {
		return submitFormInternal(form, from, null, false);
	}

	public void submitFormAsync(Form form, IPageCallback callback) {
		submitFormAsync(form, null, callback);
	}

	public void submitFormAsync(Form form, Page from, IPageCallback callback) {
		submitFormInternal(form, from, callback, true);
	}

	protected Page submitFormInternal(Form form, Page from,
			IPageCallback callback, boolean async) {
		Page page = new Page();
		page.setUrl(form.getAction());

		String fromUrl = null;
		if (from != null)
			fromUrl = from.getUrl();

		HttpRequestBase request = null;
		try {
			page.setUrl(HttpUtil.checkAndFillHttpUrl(page.getUrl(), fromUrl));
			form.normalize(from, getClientOptions());
			request = form.createHttpRequest(page);
		} catch (IOException e) {
			throw new SystemException(
					"Error creating http request for form. Bad form data", e);
		}

		String logMsg = "Submitting form to " + page.getUrl();
		if (async) {
			AsyncConnProcess<Page> process = new AsyncConnProcess<Page>(page,
					fromUrl, request, callback, logMsg);
			getAsyncExecutor().execute(process);
			return null;
		} else {
			processBaseConn(page, fromUrl, request, logMsg);
			return page;
		}
	}

	public void xhrCall(XhrCall call) {
		xhrCall(call, null);
	}

	public void xhrCall(XhrCall call, Page from) {
		xhrCallInternal(call, from, null, false);
	}

	public void xhrCallAsync(XhrCall call, IXhrCallback callback) {
		xhrCallAsync(call, null, callback);
	}

	public void xhrCallAsync(XhrCall call, Page from, IXhrCallback callback) {
		xhrCallInternal(call, from, callback, true);
	}

	protected void xhrCallInternal(XhrCall call, Page from,
			IXhrCallback callback, boolean async) {
		String fromUrl = null;
		if (from != null)
			fromUrl = from.getUrl();

		try {
			call.setUrl(HttpUtil.checkAndFillHttpUrl(call.getUrl(), fromUrl));
		} catch (MalformedURLException e) {
			throw new SystemException("Bad url in the from page", e);
		}

		call.normalize(from, getClientOptions());
		HttpRequestBase request = call.createHttpRequest(getClientOptions());
		String logMsg = "Making a " + call.getMethod() + " request call to "
				+ call.getUrl();
		if (async) {
			AsyncConnProcess<XhrCall> process = new AsyncConnProcess<XhrCall>(
					call, fromUrl, request, callback, logMsg);
			getAsyncExecutor().execute(process);
		} else {
			processBaseConn(call, fromUrl, request, logMsg);
		}
	}

	public DownloadItem download(String url, String destDir) {
		return download(url, destDir, null, null, null);
	}

	public DownloadItem download(String url, String destDir, String destName) {
		return download(url, destDir, destName, null, null);
	}

	public DownloadItem download(String url, String destDir, String destName,
			Page from) {
		return download(url, destDir, destName, from, null);
	}

	public DownloadItem download(String url, String destDir, String destName,
			Page from, IDownloadCallback callback) {
		DownloadItem item = new DownloadItem(url, destDir, destName);
		if (from != null)
			item.setFromUrl(from.getUrl());
		item.setCallback(callback);
		download(item);
		return item;
	}

	public synchronized void download(DownloadItem item) {
		if (DownloadItem.STATUS_DOWNLOADING == item.getStatus()
				|| DownloadItem.STATUS_QUEUED == item.getStatus())
			return;
		item.resetResponse();
		item.setStatus(DownloadItem.STATUS_QUEUED);
		if (!downloadItems.contains(item)) {
			downloadItems.add(item);
		}
		DownloadProcess process = new DownloadProcess(item);
		getAsyncExecutor().execute(process);
	}

	public synchronized void cancelDownload(DownloadItem item) {
		if (DownloadItem.STATUS_QUEUED != item.getStatus()
				&& DownloadItem.STATUS_DOWNLOADING != item.getStatus()) {
			return;
		}
		if (DownloadItem.STATUS_DOWNLOADING == item.getStatus()) {
			item.setInterrupted(true);
		}
		item.setStatus(DownloadItem.STATUS_NOT_STARTED);
	}

	public synchronized DownloadItem[] getAllDownloadItems() {
		DownloadItem[] ret = new DownloadItem[downloadItems.size()];
		downloadItems.toArray(ret);
		return ret;
	}

	public synchronized DownloadItem[] getDownloadItems(int status) {
		List<DownloadItem> retList = new ArrayList<DownloadItem>();
		for (DownloadItem di : downloadItems) {
			if (status == di.getStatus())
				retList.add(di);
		}
		DownloadItem[] ret = new DownloadItem[retList.size()];
		retList.toArray(ret);
		return ret;
	}

	public synchronized void removeDownloadItem(DownloadItem item) {
		downloadItems.remove(item);
	}

	public synchronized void clearDownloadItems() {
		downloadItems.clear();
	}

	class AsyncConnProcess<T extends BaseConn> implements Runnable {
		private final T conn;
		private final HttpRequestBase request;
		private final String fromUrl;
		private final IConnCallback<T> callback;
		private final String logMsg;

		public AsyncConnProcess(T conn, String fromUrl,
				HttpRequestBase request, IConnCallback<T> callback,
				String logMsg) {
			this.conn = conn;
			this.fromUrl = fromUrl;
			this.request = request;
			this.callback = callback;
			this.logMsg = logMsg;
		}

		public void run() {
			callback.onStart(conn);
			processBaseConn(conn, fromUrl, request, logMsg);
			callback.onFinish(conn);
		}
	}

	class DownloadProcess implements Runnable {
		private DownloadItem item;

		public DownloadProcess(DownloadItem item) {
			this.item = item;
		}

		private void processHttpHead(String fromUrl) throws IOException {
			prepareBasicRequestHeaders(item, fromUrl);
			HttpRequestBase request = item
					.createHeadRequest(getClientOptions());
			HttpResponse response = httpExecute(request, item);
			ResponseHeaders resHds = new ResponseHeaders();
			resHds.setHeaders(response.getAllHeaders());
			Long len = resHds.getContentLength();
			if (len != null)
				item.setExpectedSize(len);
			if (item.getDestName() == null) {
				String name = EncodeUtil.uuid();
				String ct = resHds.getContentType();
				if (ct != null) {
					String ctn = HttpUtil.parseMediaTypeFromContentType(ct);
					String ext = WELL_KNOWN_TYPE.get(ctn);
					if (ext != null) {
						name += "." + ext;
					}
				}
				item.setDestName(name);
			}
			HttpEntity entity = response.getEntity();
			if (entity != null)
				EntityUtils.consume(entity);
		}

		private void processHttp() throws IOException {
			String fromUrl = item.getFromUrl();
			item.setUrl(HttpUtil.checkAndFillHttpUrl(item.getUrl(), fromUrl));
			processHttpHead(fromUrl);

			prepareBasicRequestHeaders(item, fromUrl);
			HttpRequestBase request = item
					.createHttpRequest(getClientOptions());
			HttpResponse response = httpExecute(request, item);
			handleResponseStatus(response, item);
			if (item.getExpectedSize() < 0) {
				Long len = item.getResponseHeaders().getContentLength();
				if (len != null)
					item.setExpectedSize(len);
			}

			OutputStream os = item.createResponseStream();
			handleResponseData(response, item, os);
			item.handleResponseContent(os, getClientOptions());
		}

		private void processUrlConn() throws IOException {
			//TODO support for none http://.
		}

		public void run() {
			if (item == null)
				return;

			item.setStatus(DownloadItem.STATUS_DOWNLOADING);
			boolean repeat = false;
			int times = 1;
			int maxTimes = getClientOptions().getFailRepeatTimes();
			do {
				try {
					item.resetResponse();
					if (item.getCallback() != null) {
						item.getCallback().onStart(item);
					}

					String schema = HttpUtil.getUrlSchema(item.getUrl());
					if (schema != null) {
						if (URL_SCHEMA_HTTP.equalsIgnoreCase(schema))
							processHttp();
						else
							processUrlConn();
					} else {
						processHttp();
					}
					repeat = false;
				} catch (IOException e) {
					item.setException(e);
				}

				String logMsg = "Downloading file " + item.getUrl();
				logAfterResponse(item, (times == 1) ? logMsg : "Repeating "
						+ logMsg);

				if (item.isError()) {
					if (times < maxTimes) {
						times++;
						repeat = true;
						continue;
					}
				}
				if (item.isInterrupted()) {
					if (item.getCallback() != null) {
						item.getCallback().onCancel(item);
					}
				} else {
					item.setStatus(DownloadItem.STATUS_FINISHED);
					if (item.getCallback() != null) {
						item.getCallback().onFinish(item);
					}
				}

			} while (repeat);
		}

	}
}
